#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <filesystem>
#include <fstream>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#define _USE_MATH_DEFINES
#include <array>
#include <cmath>

#include "camera.hpp"
#include "model.hpp"
#include "shader.hpp"
#include "texture.hpp"
#include "wavelet.h"
#include <thread>

class GLTimer {
   public:
    GLTimer() : start(0), end(0) {
        glGenQueries(2, queryID);
    }
    void setStart() {
        glQueryCounter(queryID[0], GL_TIMESTAMP);
    };
    void setEnd() {
        glQueryCounter(queryID[1], GL_TIMESTAMP);
    };
    void wait() {
        GLint stop_timer_available = 0;
        while (!stop_timer_available) {
            glGetQueryObjectiv(queryID[1], GL_QUERY_RESULT_AVAILABLE, &stop_timer_available);
        }
    }
    float getTime_ms() {
        glGetQueryObjectui64v(queryID[0], GL_QUERY_RESULT, &start);
        glGetQueryObjectui64v(queryID[1], GL_QUERY_RESULT, &end);
        return (end - start) / 1000000.0;
    }

   private:
    GLuint64 start, end;
    GLuint queryID[2];
};

struct Light {
    glm::vec4 position;
    glm::vec4 color;
    float radius;
    float dummy[3];  // for alignment with std430
};

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow* window);
unsigned int loadTexture(const char* path, bool gammaCorrection);
void renderQuad();
void renderCube();
GLenum glCheckError_(const char* file, int line);
#define glCheckError() glCheckError_(__FILE__, __LINE__)

// settings
// --------------------------------
int view_mode = 1;
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 800;

// camera
// --------------------------------
Camera camera(glm::vec3(0.0f, 0.0f, 1.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;
const float move_speed = 0.005;

namespace {
    const unsigned int kernel_w = 512;
    const unsigned int kernel_h = 512;
    const unsigned int tex_w = 4096;
    const unsigned int tex_h = 4096;
    const unsigned int coef_n = 512;
    const unsigned int file_coef_n = 512;
    const float s = 23.65;
    float area = 1;
}  // namespace

const std::string sstx_filename("mike_ncoef512_128_s2365_sorted_test.sstx");

enum class RenderingMode {
    SSS,
    HAAR,
    GROUNDTRUTH,
    TRACE
};

RenderingMode mode = RenderingMode::SSS;

float f(int x) {
    x *= 2;
    float multiplier = 1.0;
    float s = sqrt(2.0);
    for (; x < kernel_w; x *= 2) {
        multiplier *= s;
    }
    return multiplier;
};

int main(int argc, char** argv) {
    unsigned concurrent_count = thread::hardware_concurrency();
	printf("Concurrent count: %d\n", concurrent_count);
    // parameter
    // --------------------------------
    for (int i = 1; i < argc; i++) {
        if (!strcmp(argv[i], "-sss")) {
            mode = RenderingMode::SSS;
        } else if (!strcmp(argv[i], "-haar")) {
            mode = RenderingMode::HAAR;
        } else if (!strcmp(argv[i], "-groundtruth")) {
            mode = RenderingMode::GROUNDTRUTH;
        } else if (!strcmp(argv[i], "-trace")) {
            mode = RenderingMode::TRACE;
        }
    }
    // glfw: initialize and configure
    // --------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // glfw: window creation and context setting
    // --------------------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "SSS", NULL, NULL);
    if (window == NULL) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    // --------------------------------
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // --------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // Check opengl environment parameters.
    // --------------------------------
    std::cout << glGetString(GL_VERSION) << std::endl;
    int workGroupSizes[3] = {0};
    glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 0, &workGroupSizes[0]);
    glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 1, &workGroupSizes[1]);
    glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, 2, &workGroupSizes[2]);
    int workGroupCounts[3] = {0};
    glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, &workGroupCounts[0]);
    glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 1, &workGroupCounts[1]);
    glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 2, &workGroupCounts[2]);
    int workGroupInvocations;
    glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &workGroupInvocations);
    std::cout << "workGroupSizes: [" << workGroupSizes[0] << ", " << workGroupSizes[1] << ", " << workGroupSizes[2] << "]" << std::endl;
    std::cout << "workGroupCounts: [" << workGroupCounts[0] << ", " << workGroupCounts[1] << ", " << workGroupCounts[2] << "]" << std::endl;
    std::cout << "workGroupInvocations: " << workGroupInvocations << std::endl;
    std::cout << "--------------------------------" << std::endl;

    // // tell stb_image.h to flip loaded texture's on the y-axis (before loading model).
    // // --------------------------------
    stbi_set_flip_vertically_on_load(false);

    // configure global opengl state
    // --------------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // --------------------------------
    // - main passes
    Shader sHaarPass1("shader/HaarPass1.vs.glsl", "shader/HaarPass1.fs.glsl");
    Shader sHaarPass2("shader/HaarPass2.vs.glsl", "shader/HaarPass2.fs.glsl");
    Shader sHaarPass3("shader/HaarPass3.cs.glsl");
    Shader sRenderPass1("shader/RenderPass1.vs.glsl", "shader/RenderPass1.fs.glsl");
    Shader sRenderPass2("shader/RenderPass2.cs.glsl");
    Shader sRenderPass3("shader/RenderPass3.vs.glsl", "shader/RenderPass3.fs.glsl");
    Shader sGroundTruthKernel("shader/GroundTruthKernel.cs.glsl");
    Shader sGroundTruthConv("shader/GroundTruthConv.cs.glsl");
    Shader sGroundTruthRender("shader/Common.vs.glsl", "shader/GroundTruthRender.fs.glsl");
    Shader sTrace("shader/Trace.cs.glsl");
    // - verification tools
    Shader sCheckImage("shader/CheckImage.vs.glsl", "shader/CheckImage.fs.glsl");
    Shader sConvolveCoef("shader/ConvolveCoef.vs.glsl", "shader/ConvolveCoef.fs.glsl");
    Shader sInverseHaar("shader/InverseHaar.cs.glsl");

    // load models
    // --------------------------------
    // Model smith(std::filesystem::current_path().string() + "/resource/smith/head.obj");
    Model mike(std::filesystem::current_path().string() + "/resource/Mike/DigitalHuman.fbx");
    // Model backpack(std::filesystem::current_path().string() + string("/resource/backpack/backpack.obj"));

    // load textures
    // --------------------------------
    GLuint smith_diffuse = loadJPG("resource/smith/textures/lambertian.jpg", true);
    GLuint smith_normal = loadJPG("resource/smith/textures/normal.png", true);
    GLuint mike_diffuse = loadJPG("resource/Mike/head_color_map_001.png", false);
    GLuint mike_normal = loadJPG("resource/Mike/head_normal_map_001.bmp", false);

    // Framebuffer and texture generation.
    // --------------------------------
    GLuint fb_world_pos_map, fb_kernel, fb_radiance_map, fb_radiance_map_after_sss;
    GLuint tssss_radiance_map, tssss_radiance_map_after_haar, tssss_radiance_map_after_sss, tssss_world_pos_map, tssss_kernel, haar_wavelet_temp_image;
    // Framebuffer for pass 1
    glGenFramebuffers(1, &fb_world_pos_map);
    glBindFramebuffer(GL_FRAMEBUFFER, fb_world_pos_map);
    // - world position map
    glGenTextures(1, &tssss_world_pos_map);
    glBindTexture(GL_TEXTURE_2D, tssss_world_pos_map);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kernel_w, kernel_h, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tssss_world_pos_map, 0);
    GLuint attachments[1] = {GL_COLOR_ATTACHMENT0};
    glDrawBuffers(1, attachments);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "Framebuffer not complete!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    // Framebuffer for kernel.
    glGenFramebuffers(1, &fb_kernel);
    glBindFramebuffer(GL_FRAMEBUFFER, fb_kernel);
    // kernel buffer
    glGenTextures(1, &tssss_kernel);
    glBindTexture(GL_TEXTURE_2D, tssss_kernel);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kernel_w, kernel_h, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tssss_kernel, 0);
    glDrawBuffers(1, attachments);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    // Framebuffer for radiance map
    glGenFramebuffers(1, &fb_radiance_map);
    glBindFramebuffer(GL_FRAMEBUFFER, fb_radiance_map);
    // - radiance map
    glGenTextures(1, &tssss_radiance_map);
    glBindTexture(GL_TEXTURE_2D, tssss_radiance_map);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kernel_w, kernel_h, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tssss_radiance_map, 0);
    // GLuint attachments[1] = {GL_COLOR_ATTACHMENT0};
    glDrawBuffers(1, attachments);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "Framebuffer not complete!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    // radiance map after haar
    glGenTextures(1, &tssss_radiance_map_after_haar);
    glBindTexture(GL_TEXTURE_2D, tssss_radiance_map_after_haar);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kernel_w, kernel_h, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    // Framebuffer for radiance map after sss
    glGenFramebuffers(1, &fb_radiance_map_after_sss);
    glBindFramebuffer(GL_FRAMEBUFFER, fb_radiance_map_after_sss);
    // - radiance map
    glGenTextures(1, &tssss_radiance_map_after_sss);
    glBindTexture(GL_TEXTURE_2D, tssss_radiance_map_after_sss);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kernel_w, kernel_h, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tssss_radiance_map_after_sss, 0);
    // GLuint attachments[1] = {GL_COLOR_ATTACHMENT0};
    glDrawBuffers(1, attachments);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "Framebuffer not complete!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    // temporary image during wavelet transformation
    glGenTextures(1, &haar_wavelet_temp_image);
    glBindTexture(GL_TEXTURE_2D, haar_wavelet_temp_image);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kernel_w, kernel_h, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

    // Create SSBOs
    // --------------------------------
    GLuint ssbo_kernel_coef;
    if (mode == RenderingMode::HAAR) {
        glGenBuffers(1, &ssbo_kernel_coef);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_kernel_coef);
        glBufferData(GL_SHADER_STORAGE_BUFFER, file_coef_n * sizeof(glm::vec4), nullptr, GL_DYNAMIC_DRAW);
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo_kernel_coef);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
    } else if (mode == RenderingMode::SSS || mode == RenderingMode::TRACE) {
        glGenBuffers(1, &ssbo_kernel_coef);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_kernel_coef);
        glBufferData(GL_SHADER_STORAGE_BUFFER, kernel_w * kernel_h * coef_n * sizeof(glm::vec4), nullptr, GL_DYNAMIC_DRAW);
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo_kernel_coef);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
    }

    // Initialize matrices
    // --------------------------------
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);
    glm::mat4 view = camera.GetViewMatrix();
    glm::mat4 model = glm::mat4(1.0f);

    if (mode == RenderingMode::HAAR) {
        glm::mat4 model_haar;
        model_haar = glm::mat4(1.0f);
        model_haar = glm::translate(model_haar, glm::vec3(1.0, 1.0, 1.0));
        // model_haar = glm::scale(model_haar, glm::vec3(0.01, 0.01, 0.01));

        // Pass 1
        // --------------------------------
        // Render world position map into **tssss_world_pos_map**.
        // --------------------------------
        glViewport(0, 0, kernel_w, kernel_h);
        glBindFramebuffer(GL_FRAMEBUFFER, fb_world_pos_map);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        sHaarPass1.use();
        sHaarPass1.setMat4("model", model_haar);
        mike.meshes[0].Draw(sHaarPass1);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

        GLTimer timer_haar;
        ofstream coef_file;
        // coef_file.open(sstx_filename, ios::binary);
        std::vector<float> coefs;
        coefs.resize(file_coef_n);
        std::vector<int> indices;
        indices.resize(file_coef_n);
        int coefs_base = 0;
        int cnt = 0;

        std::vector<glm::vec4> first_coefs;

        for (int row = 0; row < kernel_h; row++) {
            // std::printf("Row %d...\n", row);
            for (int col = 0; col < kernel_w; col++) {
                // std::printf("\tCol %d...\n", col);
                timer_haar.setStart();

                // Pass 2
                // --------------------------------
                // Compute a kernel.
                // --------------------------------
                glViewport(0, 0, kernel_w, kernel_h);
                glBindFramebuffer(GL_FRAMEBUFFER, fb_kernel);
                sHaarPass2.use();
                sHaarPass2.setInt("kernel_w", kernel_w);
                sHaarPass2.setInt("kernel_h", kernel_h);
                sHaarPass2.setVec2i("index_kernel_iv", glm::ivec2(col, row));
                sHaarPass2.setFloat("s", s);
                glBindImageTexture(0, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                renderQuad();
                glBindFramebuffer(GL_FRAMEBUFFER, 0);

                // Normalizing
                std::vector<glm::vec4> kernel_data;
                kernel_data.resize(kernel_h * kernel_w);
                glBindTexture(GL_TEXTURE_2D, tssss_kernel);
                glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, (void*)kernel_data.data());
                if (kernel_data[0] != glm::vec4(0, 0, 1, 0)) {
                    float sum = 0;
                    for (auto& iter : kernel_data) {
                        sum += iter.x;
                    }

                    // Pass 3
                    // --------------------------------
                    // Perform wavelet transformation to the kernel.
                    // --------------------------------
                    sHaarPass3.use();
                    sHaarPass3.setInt("coef_n", file_coef_n);
                    sHaarPass3.setInt("tex_w", kernel_w);
                    sHaarPass3.setInt("tex_h", kernel_h);
                    sHaarPass3.setFloat("sum", sum);
                    glBindImageTexture(0, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    glBindImageTexture(1, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    glBindImageTexture(2, haar_wavelet_temp_image, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    sHaarPass3.setVec2i("index_kernel_iv", glm::ivec2(col, row));
                    glDispatchCompute(1, 1, 1);
                    glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);

                    // // CPU version
                    // Haar haar;
                    // std::vector<float> data_for_wavelet;
                    // data_for_wavelet.resize(kernel_h * kernel_w);
                    // std::transform(kernel_data.begin(), kernel_data.end(), data_for_wavelet.begin(), [](glm::vec4 v) { return v.x; });
                    // haar.decomposite(data_for_wavelet, kernel_w, kernel_h);
                    // std::transform(data_for_wavelet.begin(), data_for_wavelet.end(), kernel_data.begin(), [](float x) { return glm::vec4(x, 0, 0, 0); });
                    // glBindTexture(GL_TEXTURE_2D, tssss_kernel);
                    // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kernel_w, kernel_h, 0, GL_RGBA, GL_FLOAT, kernel_data.data());
                }

                glfwSetWindowShouldClose(window, false);
                while (!glfwWindowShouldClose(window)) {
                    // process input
                    // --------------------------------
                    processInput(window);
                    // Pass
                    // --------------------------------
                    // Check image.
                    // --------------------------------
                    glBindFramebuffer(GL_FRAMEBUFFER, 0);
                    glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
                    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
                    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                    sCheckImage.use();
                    sCheckImage.setInt("check_binding", 3);
                    // glBindImageTexture(0, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    // glBindImageTexture(1, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    glBindImageTexture(3, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    glBindImageTexture(4, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    renderQuad();

                    glfwSwapBuffers(window);
                    glfwPollEvents();
                    glfwSetWindowShouldClose(window, true);
                }

                // // Sort and Write to file.
                // // --------------------------------
                // // Sort and keep first several coefs.
                // // format: empty_cnt|coefs[]|indices[]|empty_cnt|coefs[]|indices[]|empty_cnt|...
                // // --------------------------------
                // std::vector<glm::vec4> kernel_haar_data_v;
                // kernel_haar_data_v.resize(kernel_h * kernel_w);
                // glBindTexture(GL_TEXTURE_2D, tssss_kernel);
                // glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, (void*)kernel_haar_data_v.data());

                // if (kernel_haar_data_v[0] == glm::vec4(0, 0, 1, 0))  // kernel is invalid
                // {
                //     cnt++;
                // } else {
                //     auto cmp = [](glm::vec4 a, glm::vec4 b) {
                //         return abs(a.x) > abs(b.x);
                //     };
                //     std::nth_element(kernel_haar_data_v.begin(), kernel_haar_data_v.begin() + file_coef_n, kernel_haar_data_v.end(), cmp);
                //     std::sort(kernel_haar_data_v.begin(), kernel_haar_data_v.begin() + file_coef_n, cmp);
                //     for (int i = 0; i < file_coef_n; i++) {
                //         coefs[i] = kernel_haar_data_v[i].x / sum;
                //         indices[i] = (int)kernel_haar_data_v[i].y;
                //     }
                //     coef_file.write((char*)&cnt, sizeof(int));
                //     coef_file.write((char*)coefs.data(), file_coef_n * sizeof(float));
                //     coef_file.write((char*)indices.data(), file_coef_n * sizeof(int));
                //     cnt = 0;
                //     first_coefs.emplace_back(kernel_haar_data_v[0]);
                // }
                timer_haar.setEnd();
                timer_haar.wait();
                std::printf("Time spent on row %d col %d: %f ms\n", row, col, timer_haar.getTime_ms());
            }
        }
        return 0;
        coef_file << cnt;
        coef_file.close();
    }
    // Render loop
    // --------------------------------
    else if (mode == RenderingMode::SSS) {
        // Read kernel haar coefficients from file and save to SSBO.
        // --------------------------------
        std::printf("Read kernel coefficients and indices...");
        // - Open file & Set buffer.
        ifstream coef_file;
        coef_file.open((sstx_filename), ios::binary);
        // - Buffers.
        int empty_cnt = 0;
        std::vector<float> kernel_coef_buffer;
        kernel_coef_buffer.resize(file_coef_n);
        std::vector<int> kernel_index_buffer;
        kernel_index_buffer.resize(file_coef_n);
        // // - Data.
        // int coef_idx_base = 0;
        // std::vector<float> kernel_coefs{coef_n * kernel_h * kernel_w};
        // std::vector<int> kernel_indices{coef_n * kernel_h * kernel_w};
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_kernel_coef);
        glm::vec4* kernel_coef_ptr = (glm::vec4*)glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_READ_WRITE);
        coef_file.read((char*)&empty_cnt, sizeof(int));
        for (int ssbo_index = 0; ssbo_index < kernel_h * kernel_w; ssbo_index++) {
            if (empty_cnt) {
                for (int coef_index = 0; coef_index < coef_n; coef_index++) {
                    kernel_coef_ptr[ssbo_index * coef_n + coef_index] = glm::vec4(0);
                }
                empty_cnt--;
            } else {
                coef_file.read((char*)kernel_coef_buffer.data(), file_coef_n * sizeof(float));
                coef_file.read((char*)kernel_index_buffer.data(), file_coef_n * sizeof(int));
                for (int coef_index = 0; coef_index < coef_n; coef_index++) {
                    kernel_coef_ptr[ssbo_index * coef_n + coef_index] =
                        glm::vec4(kernel_coef_buffer[coef_index],
                                  (float)kernel_index_buffer[coef_index],
                                  0,
                                  0);
                }
                coef_file.read((char*)&empty_cnt, sizeof(int));
            }
        }
        glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
        coef_file.close();
        std::printf("Done.\n");

        glm::mat4 model = glm::mat4(1.0f);
        model = glm::translate(model, glm::vec3(0, -1.7, 0.5));
        model = glm::scale(model, glm::vec3(0.01, 0.01, 0.01));
        model = glm::rotate(model, glm::float32(glm::radians(90.0)), glm::vec3(-1.0, 0.0, 0.0));

        while (!glfwWindowShouldClose(window)) {
            // process input
            // --------------------------------
            processInput(window);

            // update matrices
            // --------------------------------
            projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.0001f, 100000.0f);
            view = camera.GetViewMatrix();

            // set texture
            // --------------------------------

            GLTimer timer;
            // Pass 1
            // --------------------------------
            // Render radiance map into **tssss_radiance_map**.
            // --------------------------------
            timer.setStart();
            glBindFramebuffer(GL_FRAMEBUFFER, fb_radiance_map);
            glClear(GL_COLOR_BUFFER_BIT);
            glViewport(0, 0, kernel_w, kernel_h);
            sRenderPass1.use();
            sRenderPass1.setMat4("model", model);
            sRenderPass1.setMat4("view", view);
            sRenderPass1.setMat4("projection", projection);
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, mike_diffuse);
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D, mike_normal);
            mike.meshes[0].Draw(sRenderPass1);
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
            // timer.setEnd();
            // timer.wait();
            // printf("Pass 1 Radiance map: %fms. ", timer.getTime_ms());

            // // Pass 2
            // // --------------------------------
            // // Compute haar transformation of radiance map.
            // // --------------------------------
            // // timer.setStart();
            // sRenderPass2.use();
            // sRenderPass2.setInt("tex_w", kernel_w);
            // sRenderPass2.setInt("tex_h", kernel_h);
            // glBindImageTexture(0, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(1, tssss_radiance_map_after_haar, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(2, haar_wavelet_temp_image, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glDispatchCompute(1, 1, 1);
            // glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
            timer.setEnd();
            timer.wait();
            std::printf("Radiance map haar transform: %fms.\n", timer.getTime_ms());

            Haar haar;
            std::vector<glm::vec4> radiance_map_data;
            radiance_map_data.resize(kernel_h * kernel_w);
            glBindTexture(GL_TEXTURE_2D, tssss_radiance_map);
            glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, (void*)radiance_map_data.data());
            std::vector<glm::vec4> radiance_map_after_haar_data;
            radiance_map_after_haar_data.resize(kernel_h * kernel_w);

            std::vector<float> data_for_wavelet;
            data_for_wavelet.resize(kernel_h * kernel_w);
            std::transform(radiance_map_data.begin(), radiance_map_data.end(), data_for_wavelet.begin(), [](glm::vec4 v) { return v.x; });
            haar.decomposite(data_for_wavelet, kernel_w, kernel_h);
            std::transform(data_for_wavelet.begin(), data_for_wavelet.end(), radiance_map_after_haar_data.begin(), [](float x) { return glm::vec4(x, 0, 0, 0); });
            glBindTexture(GL_TEXTURE_2D, tssss_radiance_map_after_haar);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, kernel_w, kernel_h, 0, GL_RGBA, GL_FLOAT, radiance_map_after_haar_data.data());

            // // [TEST] Single texel convolution
            // // --------------------------------
            // // --------------------------------
            // // --------------------------------
            // glm::mat4 model_haar;
            // model_haar = glm::mat4(1.0f);
            // model_haar = glm::translate(model_haar, glm::vec3(1.0, 1.0, 1.0));
            // // Pass 1
            // // --------------------------------
            // // Render world position map into **tssss_world_pos_map**.
            // // --------------------------------
            // glViewport(0, 0, tex_w, tex_h); // Do this to render texture in a larger scale rather than size of display window.
            // glBindFramebuffer(GL_FRAMEBUFFER, fb_world_pos_map);
            // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            // sHaarPass1.use();
            // sHaarPass1.setMat4("model", model_haar);
            // glActiveTexture(GL_TEXTURE0);
            // glBindTexture(GL_TEXTURE_2D, mike_diffuse);
            // mike.Draw(sHaarPass1);
            // glBindFramebuffer(GL_FRAMEBUFFER, 0);

            // GLTimer timer_haar;
            // ofstream coef_file("test.sstx", ios::binary);
            // float *coefs = new float[coef_n * tex_h * tex_w];
            // int *indices = new int[coef_n * tex_h * tex_w];
            // int coefs_base = 0;

            // sHaarPass3.use();
            // sHaarPass3.setInt("coef_n", coef_n);
            // sHaarPass3.setInt("tex_w", tex_w);
            // sHaarPass3.setInt("tex_h", tex_h);
            // glBindImageTexture(0, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(1, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(2, haar_wavelet_temp_image, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // sHaarPass3.setVec2i("index_kernel_iv", glm::ivec2(124856 / tex_w, 124856 % tex_w));
            // glDispatchCompute(1, 1, 1);
            // glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);

            // // data after haar
            // glm::vec4 *kernel_data = new glm::vec4[tex_h * tex_w];
            // glBindTexture(GL_TEXTURE_2D, tssss_kernel);
            // glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, (void *)kernel_data);
            // glm::vec4 *radiance_data = new glm::vec4[tex_h * tex_w];
            // glBindTexture(GL_TEXTURE_2D, tssss_radiance_map_after_haar);
            // glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, (void *)radiance_data);
            // glm::vec3 sum = glm::vec3(0, 0, 0);
            // for (int i = 0; i < tex_h * tex_w; i++)
            // {
            // 	sum += kernel_data[i].x * glm::vec3(radiance_data[i]);
            // }
            // return 0;
            // // --------------------------------
            // // --------------------------------
            // // --------------------------------

            // // Inverse
            // // --------------------------------
            // // Perform inverse haar transformation.
            // // --------------------------------
            // sInverseHaar.use();
            // sInverseHaar.setInt("coef_n", coef_n);
            // sInverseHaar.setInt("tex_w", tex_w);
            // sInverseHaar.setInt("tex_h", tex_h);
            // glBindImageTexture(0, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(1, haar_wavelet_temp_image, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glDispatchCompute(1, 1, 1);
            // glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);

            // // Convolution
            // // --------------------------------
            // // Convolve and output image.
            // // --------------------------------
            // timer.setStart();
            // glBindFramebuffer(GL_FRAMEBUFFER, fb_radiance_map_after_sss);
            // sConvolveCoef.use();
            // sConvolveCoef.setInt("coef_n", coef_n);
            // sConvolveCoef.setInt("kernel_w", kernel_w);
            // sConvolveCoef.setInt("kernel_h", kernel_h);
            // glActiveTexture(GL_TEXTURE0);
            // glBindTexture(GL_TEXTURE_2D, mike_diffuse);
            // // glBindImageTexture(0, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(1, tssss_radiance_map_after_haar, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // // glBindImageTexture(2, tssss_radiance_map_after_sss, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // renderQuad();
            // glBindFramebuffer(GL_FRAMEBUFFER, 0);
            // timer.setEnd();
            // timer.wait();
            // std::printf("Convolution: %fms.\n\n", timer.getTime_ms());

            // Check
            // --------------------------------
            // Check radiance map and kernels.
            // --------------------------------
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
            glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            sCheckImage.use();
            sCheckImage.setInt("check_binding", 1);
            sCheckImage.setFloat("A", area);
            glBindImageTexture(0, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            glBindImageTexture(1, tssss_radiance_map_after_haar, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            glBindImageTexture(2, tssss_radiance_map_after_sss, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(3, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(4, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            renderQuad();

            // // Pass 3
            // // --------------------------------
            // // render.
            // // --------------------------------
            // glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
            // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            // glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            // sRenderPass3.use();
            // sRenderPass3.setInt("mode", view_mode);
            // sRenderPass3.setMat4("model", model);
            // sRenderPass3.setMat4("view", view);
            // sRenderPass3.setMat4("projection", projection);
            // sRenderPass3.setVec3("view_pos", camera.Position);
            // sRenderPass3.setInt("coef_n", coef_n);
            // sRenderPass3.setInt("area", area);
            // sRenderPass3.setInt("tex_w", tex_w);
            // sRenderPass3.setInt("tex_h", tex_h);
            // sRenderPass3.setInt("kernel_w", kernel_w);
            // sRenderPass3.setInt("kernel_h", kernel_h);
            // sRenderPass3.setFloat("A", area);
            // glActiveTexture(GL_TEXTURE0);
            // glBindTexture(GL_TEXTURE_2D, mike_diffuse);
            // glActiveTexture(GL_TEXTURE1);
            // glBindTexture(GL_TEXTURE_2D, mike_normal);
            // glActiveTexture(GL_TEXTURE31);
            // glBindTexture(GL_TEXTURE_2D, tssss_radiance_map_after_sss);
            // // glBindImageTexture(0, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // // glBindImageTexture(1, tssss_radiance_map_after_haar, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // // glBindImageTexture(2, tssss_radiance_map_after_sss, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // mike.meshes[0].Draw(sRenderPass3);

            glfwSwapBuffers(window);
            glfwPollEvents();
        }
    } else if (mode == RenderingMode::GROUNDTRUTH) {
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::translate(model, glm::vec3(0, -1.7, 0.5));
        model = glm::rotate(model, glm::float32(glm::radians(90.0)), glm::vec3(-1.0, 0.0, 0.0));

        GLTimer timer;

        // --------------------------------
        // Render radiance map into **tssss_radiance_map**.
        // --------------------------------
        glBindFramebuffer(GL_FRAMEBUFFER, fb_radiance_map);
        glViewport(0, 0, kernel_w, kernel_h);
        sRenderPass1.use();
        sRenderPass1.setMat4("model", model);
        sRenderPass1.setMat4("view", view);
        sRenderPass1.setMat4("projection", projection);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, mike_diffuse);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, mike_normal);
        mike.meshes[0].Draw(sRenderPass1);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        // --------------------------------
        // Render world position map into **tssss_world_pos_map**.
        // --------------------------------
        glViewport(0, 0, kernel_w, kernel_h);  // Do this to render texture in a larger scale rather than size of display window.
        glBindFramebuffer(GL_FRAMEBUFFER, fb_world_pos_map);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        sHaarPass1.use();
        sHaarPass1.setMat4("model", model);
        mike.meshes[0].Draw(sHaarPass1);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

        for (int row = 250; row < kernel_h; row++) {
            for (int col = 0; col < kernel_w; col++) {
                timer.setStart();
                // --------------------------------
                // Compute a kernel.
                // --------------------------------
                glViewport(0, 0, kernel_w, kernel_h);
                glBindFramebuffer(GL_FRAMEBUFFER, fb_kernel);
                sHaarPass2.use();
                sHaarPass2.setInt("kernel_w", kernel_w);
                sHaarPass2.setInt("kernel_h", kernel_h);
                sHaarPass2.setVec2i("index_kernel_iv", glm::ivec2(col, row));
                glBindImageTexture(0, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                renderQuad();
                glBindFramebuffer(GL_FRAMEBUFFER, 0);

                // --------------------------------
                // Convolve and save color.
                // --------------------------------
                sGroundTruthConv.use();
                sGroundTruthConv.setInt("kernel_w", kernel_w);
                sGroundTruthConv.setInt("kernel_h", kernel_h);
                sGroundTruthConv.setVec2i("index_kernel_iv", glm::ivec2(col, row));
                glBindImageTexture(0, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                glBindImageTexture(1, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                glBindImageTexture(2, tssss_radiance_map_after_sss, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                glDispatchCompute(1, 1, 1);
                glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);

                glfwSetWindowShouldClose(window, false);
                while (!glfwWindowShouldClose(window)) {
                    processInput(window);
                    // --------------------------------
                    // Check image.
                    // --------------------------------
                    glBindFramebuffer(GL_FRAMEBUFFER, 0);
                    glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
                    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
                    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
                    sCheckImage.use();
                    sCheckImage.setInt("check_binding", 2);
                    sCheckImage.setInt("A", area);
                    glBindImageTexture(0, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    // glBindImageTexture(1, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    glBindImageTexture(2, tssss_radiance_map_after_sss, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    glBindImageTexture(3, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    // glBindImageTexture(4, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
                    renderQuad();

                    glfwSwapBuffers(window);
                    glfwPollEvents();
                    glfwSetWindowShouldClose(window, true);
                }

                timer.setEnd();
                timer.wait();
                std::printf("Time spent on row %d col %d: %f ms\n", row, col, timer.getTime_ms());
            }
        }

        while (!glfwWindowShouldClose(window)) {
            // process input
            // --------------------------------
            processInput(window);

            // update matrices
            // --------------------------------
            projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.0001f, 100000.0f);
            view = camera.GetViewMatrix();
            // --------------------------------
            // Render.
            // --------------------------------
            glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            sGroundTruthRender.use();
            sGroundTruthRender.setMat4("model", model);
            sGroundTruthRender.setMat4("view", view);
            sGroundTruthRender.setMat4("projection", projection);
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, mike_diffuse);
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D, mike_normal);
            glBindImageTexture(0, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            glBindImageTexture(1, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            sGroundTruthRender.setInt("mode", view_mode);
            sGroundTruthRender.setVec3("view_pos", camera.Position);
            sGroundTruthRender.setInt("area", area);
            sGroundTruthRender.setInt("tex_w", tex_w);
            sGroundTruthRender.setInt("tex_h", tex_h);
            mike.meshes[0].Draw(sGroundTruthRender);
            // renderQuad();

            glfwSwapBuffers(window);
            glfwPollEvents();
        }
    } else if (mode == RenderingMode::TRACE) {
        glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
        // Read kernel haar coefficients from file and save to SSBO.
        // --------------------------------
        std::printf("Read kernel coefficients and indices...");
        // - Open file & Set buffer.
        ifstream coef_file;
        coef_file.open((sstx_filename), ios::binary);
        int coef_n = file_coef_n * kernel_h * kernel_w;
        float* kernel_coef_buffer = new float[coef_n];
        int* kernel_index_buffer = new int[coef_n];
        // - Read.
        coef_file.read((char*)kernel_coef_buffer, coef_n * sizeof(float));
        coef_file.read((char*)kernel_index_buffer, coef_n * sizeof(int));
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo_kernel_coef);
        glm::vec4* kernel_coef_ptr = (glm::vec4*)glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_READ_WRITE);
        for (int ssbo_index = 0; ssbo_index < kernel_h * kernel_w; ssbo_index++) {
            for (int coef_index = 0; coef_index < coef_n; coef_index++) {
                kernel_coef_ptr[ssbo_index * coef_n + coef_index].r = kernel_coef_buffer[ssbo_index * file_coef_n + coef_index];
                kernel_coef_ptr[ssbo_index * coef_n + coef_index].g = (float)kernel_index_buffer[ssbo_index * file_coef_n + coef_index];
                kernel_coef_ptr[ssbo_index * coef_n + coef_index].b = 0;
                kernel_coef_ptr[ssbo_index * coef_n + coef_index].a = 0;
            }
        }
        glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
        delete[] kernel_coef_buffer;
        delete[] kernel_index_buffer;
        coef_file.close();
        std::printf("Done.\n");

        while (!glfwWindowShouldClose(window)) {
            // process input
            // --------------------------------
            processInput(window);

            sTrace.use();
            double xpos, ypos;
            glfwGetCursorPos(window, &xpos, &ypos);
            cout << xpos << " " << ypos << endl;
            sTrace.setFloat("x", float(xpos / SCR_WIDTH * kernel_w));
            sTrace.setFloat("y", float(ypos / SCR_HEIGHT * kernel_h));
            sTrace.setInt("kernel_w", kernel_w);
            sTrace.setInt("kernel_h", kernel_h);
            sTrace.setInt("coef_n", coef_n);
            glBindImageTexture(0, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            glDispatchCompute(1, 1, 1);

            // Check
            // --------------------------------
            // Check radiance map and kernels.
            // --------------------------------
            glBindFramebuffer(GL_FRAMEBUFFER, 0);
            glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            sCheckImage.use();
            sCheckImage.setInt("check_binding", 3);
            sCheckImage.setInt("A", area);
            // glBindImageTexture(0, tssss_radiance_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(1, tssss_radiance_map_after_haar, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(2, tssss_radiance_map_after_sss, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            glBindImageTexture(3, tssss_kernel, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            // glBindImageTexture(4, tssss_world_pos_map, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
            renderQuad();

            glfwSwapBuffers(window);
            glfwPollEvents();
        }
        glfwTerminate();
        return 0;
    }
}

// renderCube() renders a 1x1 3D cube in NDC.
// -------------------------------------------------
GLuint cubeVAO = 0;
GLuint cubeVBO = 0;
void renderCube() {
    // initialize (if necessary)
    if (cubeVAO == 0) {
        float vertices[] = {
            // back face
            -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,  // bottom-left
            1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,    // top-right
            1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,   // bottom-right
            1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,    // top-right
            -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,  // bottom-left
            -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,   // top-left
            // front face
            -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,  // bottom-left
            1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,   // bottom-right
            1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,    // top-right
            1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,    // top-right
            -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,   // top-left
            -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,  // bottom-left
            // left face
            -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,    // top-right
            -1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,   // top-left
            -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,  // bottom-left
            -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,  // bottom-left
            -1.0f, -1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,   // bottom-right
            -1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,    // top-right
                                                                 // right face
            1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,      // top-left
            1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,    // bottom-right
            1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,     // top-right
            1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,    // bottom-right
            1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,      // top-left
            1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,     // bottom-left
                                                                 // bottom face
            -1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,  // top-right
            1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,   // top-left
            1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,    // bottom-left
            1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,    // bottom-left
            -1.0f, -1.0f, 1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,   // bottom-right
            -1.0f, -1.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,  // top-right
                                                                 // top face
            -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,    // top-left
            1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,      // bottom-right
            1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,     // top-right
            1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,      // bottom-right
            -1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,    // top-left
            -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f      // bottom-left
        };
        glGenVertexArrays(1, &cubeVAO);
        glGenBuffers(1, &cubeVBO);
        // fill buffer
        glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        // link vertex attributes
        glBindVertexArray(cubeVAO);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
                              (void*)0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
                              (void*)(3 * sizeof(float)));
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
                              (void*)(6 * sizeof(float)));
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
    }
    // render Cube
    glBindVertexArray(cubeVAO);
    glDrawArrays(GL_TRIANGLES, 0, 36);
    glBindVertexArray(0);
}

// renderQuad() renders a 1x1 XY quad in NDC
// -----------------------------------------
GLuint quadVAO = 0;
GLuint quadVBO = 0;
void renderQuad() {
    if (quadVAO == 0) {
        float quadVertices[] = {
            // positions        // texture Coords
            -1.0f,
            1.0f,
            0.0f,
            0.0f,
            1.0f,
            -1.0f,
            -1.0f,
            0.0f,
            0.0f,
            0.0f,
            1.0f,
            1.0f,
            0.0f,
            1.0f,
            1.0f,
            1.0f,
            -1.0f,
            0.0f,
            1.0f,
            0.0f,
        };
        // setup plane VAO
        glGenVertexArrays(1, &quadVAO);
        glGenBuffers(1, &quadVBO);
        glBindVertexArray(quadVAO);
        glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices,
                     GL_STATIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
                              (void*)0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
                              (void*)(3 * sizeof(float)));
    }
    glBindVertexArray(quadVAO);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    glBindVertexArray(0);
}

// process all input: query GLFW whether relevant keys are pressed/released this
// frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, move_speed);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, move_speed);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, move_speed);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, move_speed);
    if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS)
        camera.ProcessKeyboard(UP, move_speed);
    if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS)
        camera.ProcessKeyboard(DOWN, move_speed);
    if (glfwGetKey(window, GLFW_KEY_PAGE_UP) == GLFW_PRESS && area < 1000000)
        area *= 1.5;
    if (glfwGetKey(window, GLFW_KEY_PAGE_DOWN) == GLFW_PRESS && area > 0.002)
        area /= 1.5;
    if (glfwGetKey(window, GLFW_KEY_0) == GLFW_PRESS)
        view_mode = 0;
    if (glfwGetKey(window, GLFW_KEY_1) == GLFW_PRESS)
        view_mode = 1;
}

// glfw: whenever the window size changed (by OS or user resize) this callback
// function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    // make sure the viewport matches the new window dimensions; note that width
    // and height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    if (firstMouse) {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset =
        lastY - ypos;  // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
    camera.ProcessMouseScroll(yoffset);
}

// OpenGL error check function.
// --------------------------------
GLenum glCheckError_(const char* file, int line) {
    GLenum errorCode;
    while ((errorCode = glGetError()) != GL_NO_ERROR) {
        std::string error;
        switch (errorCode) {
            case GL_INVALID_ENUM:
                error = "INVALID_ENUM";
                break;
            case GL_INVALID_VALUE:
                error = "INVALID_VALUE";
                break;
            case GL_INVALID_OPERATION:
                error = "INVALID_OPERATION";
                break;
            case GL_STACK_OVERFLOW:
                error = "STACK_OVERFLOW";
                break;
            case GL_STACK_UNDERFLOW:
                error = "STACK_UNDERFLOW";
                break;
            case GL_OUT_OF_MEMORY:
                error = "OUT_OF_MEMORY";
                break;
            case GL_INVALID_FRAMEBUFFER_OPERATION:
                error = "INVALID_FRAMEBUFFER_OPERATION";
                break;
        }
        std::cout << error << " | " << file << " (" << line << ")" << std::endl;
    }
    return errorCode;
}